//
//	MiscMergeCommand.m -- abstract class to build merge commands from
//		Written by Don Yacktman Copyright (c) 1995 by Don Yacktman.
//				Version 1.0.  All rights reserved.
//		This notice may not be removed from this source code.
//
//	This object is included in the MiscKit by permission from the author
//	and its use is governed by the MiscKit license, found in the file
//	"LICENSE.rtf" in the MiscKit distribution.  Please refer to that file
//	for a list of all applicable permissions and restrictions.
//	


#import <misckit/MiscMergeCommand.h>
#import <misckit/misckit.h>

@class MiscMergeEngine;

@interface MiscMergeCommand(private)
- _getQuotedArgumentStringFrom:(MiscString *)aString toEnd:(BOOL)endFlag;
@end

@implementation MiscMergeCommand
/*" The MiscMergeCommand class implements a merge command.  Since the
MiscKit merge engine can dynamically add new commands, it is possible
to create custom subclasses of MiscMergeCommand to implement new
functionality in the engine.  The merge engine already implements
most of the commands a user would want, but certain applications may
wish to override those commands, replace them, or add new commands.

To create a subclass, two methods, -#{parseFromString:} and
-#{executeForMerge:} need to be implemented.  The first is expected to
break up a text string into whatever arguments a particular MiscMergeCommand
subclass needs in order to function.  The second performs, during a merge,
whatever special task the command is designed to do.

The other methods in this object may be used by subclasses to aid in
parsing the command string.  They can grab key words, conditional operators,
an arguments (single word or quoted string).  A special kind of argument,
promptable, is also supported.  A promptable argument is expected to have
its actual value determined at run time.

When implementing commands, the full API of the MiscMergeEngine object is
available.  This allows the programmer to store information in the engine,
manipulate the symbol tables used for resolving fields, and alter the
output being created by the merge.

The MiscKit source code is a good place to look for examples of how
to implement various MiscMergeCommand subclasses.
"*/

- init
/*" Causes an error, since the -#{initFrom:} method %{must} be called
to initialize MiscMergeCommands.  Frees self and returns nil.
"*/
{
	[super init];
	[self error:"Must use -initFrom: with MiscMergeCommand classes!\n"];
	[self free];
	return nil;
}

- initFrom:aString
/*" Initializes a new instance of MiscMergeCommand, based upon the text
of %{aString}.  Actual parsing is performed by the -#{parseFromString:}
method.  If parsing fails and returns nil, then this method will free
the receiving instance and return nil.  Returns self otherwise.
"*/
{
	[super init];
	if (![self parseFromString:aString]) { // fatal parse error
		[self free];
		return nil;
	}
	return self;
}

- parseFromString:(MiscString *)aString
/*" This method is called while parsing a merge template.  The text of
the full merge command is contained in %{aString}.  This method should
break %{aString} up into keywords, conditionals, and arguments as
needed and store the results in instance variables for later use during
merges.  Note that returning self tells the template parsing machinery
that all is well.  Return nil if there is an error or the command cannot
be properly initialized.  (But do %{not} call -#{free} on self if nil
is returned!)
"*/
{
	// subclass responsibility
	return nil;
}

- executeForMerge:(MiscMergeEngine *)aMerger
/*"  This method is called by the merge engine while it is
performing a merge.  The command is expected to perform it's
specified function when this call is received.  Returns self.  The
return value is currently ignored by the caller, which is usually,
but does not have to be, %{aMerger}.
"*/
{
	// subclass responsibility
	return nil;
}

- (BOOL)eatKeyWord:(MiscString *)aKeyWord from:(MiscString *)aString
	isOptional:(BOOL)flag
/*" Attempts to remove the contents of %{aKeyWord} from %{aString}.
If %{flag} is YES, then no complaint will be made if %{aKeyWord} is
missing.  YES or NO is returned to tell the caller if the required
key word was found or not, no matter what the value of %{flag} was.
Note that it %{aKeyWord} is optional, then it will only be found
if %{aString} has no whitespace at the beginning.  This is a design
decision, since the merger has a few pseudo-commands, such as copy,
which could give a less desireable behavior if the whitespace were
trimmed from the start of %{aString}.  Since user-implemented
MiscMergeCommand subclasses should pass YES for %{flag} this
subtle design decision should not have a major impact.
"*/
{
// If the keyword is optional, it may be implied, such as the "copy"
// pseudo-command.  If that is the case, then we don't want to alter
// the string at all.  So a copy command with the keyword in place
// must not have any leading whitespace, since we won't attempt to
// trim it.  This can leave unwanted keywords in the parse stream
// if you aren't careful... This is a problem with the design and
// not a bug.  This implementation will disturb things less than if
// we always did a trim, so I prefer it.
	if (!flag) [aString trimLeadWhiteSpaces];
	if (![aString compareTo:aKeyWord n:[aKeyWord length]
			caseSensitive:NO]) {
		[aString removeFrom:0 length:[aKeyWord length]];
		return YES;
	}
	if (!flag) {
		[self error_keyword:aKeyWord];
	}
	return NO;
}

- _getQuotedArgumentStringFrom:(MiscString *)aString toEnd:(BOOL)endFlag
{
	id ret = nil; int count = 0;

	[aString trimLeadWhiteSpaces];
	if ([aString charAt:0] == '"') {
		[aString removeFrom:0 length:1];
	}

	if (endFlag) { // eat to the end, regardless.
		[aString trimTailWhiteSpaces];
		if ([aString charAt:([aString length]-1)] == '"') {
			ret = [aString midFrom:0 length:([aString length]-1)];
		} else {
			[self error_closequote];
			ret = [aString copy];
		}
		[aString setStringValue:""];
		return ret;
	}

	while (!ret) {
		int right = [aString spotOf:'"'
				occurrenceNum:count caseSensitive:NO] - 1;
		if (right < -1) {
			ret = [aString copy];
			[self error_closequote];
		}
		if (right == -1) {
			ret = [MiscString new];
			[self error_closequote];
		}
		if ([aString charAt:(right - 1)] == '\\') {
			count += 1;
		} else {
			ret = [aString midFrom:0 to:right];
		}
	}
	[aString replace:[ret stringValue] with:""];
	return ret;
}

- getArgumentStringFrom:(MiscString *)aString toEnd:(BOOL)endFlag
/*" Attempts to parse an argument from %{aString}.  If %{endFlag}
is set, then whatever is found to the end of %{aString} will be
assumend to be the  required argument.  Otherwise, if an argument
contains whitespace, it should be surrounded by quotation marks ª"º.
The parsed argument will be removed from %{aString}.
"*/
{
	[aString trimLeadWhiteSpaces];
	if ([aString charAt:0] == '"') {
		return [self _getQuotedArgumentStringFrom:aString toEnd:endFlag];
	} else {
		if (!endFlag) {
			id ret;
			int brk = [aString spotOfChars:" \t\n<>!=" occurrenceNum:0];
			if (brk <= 0) { // goes to end anyway
				id ret = [aString copy];
				[aString setStringValue:""];
				return ret;
			}
			ret = [aString midFrom:0 to:brk];
			[aString replace:[ret stringValue] with:""];
			return ret;
		} else {
			id ret = [aString copy];
			[aString setStringValue:""];
			return ret;
		}
	}
	return nil;
}

- getPromptFrom:(MiscString *)aString toEnd:(BOOL)endFlag
/*" Attempts to parse a promptable argument from %{aString}.  If the
argument begins with a "?" then the argument is a "prompt".  Removes
the parsed argument from %{aString} and returns it.  Returns nil if
the wrong kind of argument was found.  It is expected that a promptable
argument's value will be determined at run time by asking the user for
the value that should be stored for it.
"*/
{
	[aString trimLeadWhiteSpaces];
	if ([aString charAt:0] == '?') {
		[aString removeFrom:0 length:1];
		return [self getArgumentStringFrom:aString toEnd:endFlag];
	} else {
		[self error_noprompt];
		return nil;
	}
	return nil;
}

- getPromptableArgumentStringFrom:(MiscString *)aString
		wasPrompt:(BOOL *)prompt toEnd:(BOOL)endFlag
/*" Attempts to parse an argument, which could be promptable, from %{aString}.
If the argument begins with a "?" then the argument is a "prompt".  Otherwise,
a regular argument is parsed.  Removes the parsed argument from %{aString}
and returns it.  %{prompt} is set to YES or NO depending upon what was parsed.
"*/
{
	[aString trimLeadWhiteSpaces];
	if ([aString charAt:0] == '?') {
		*prompt = NO;
		return [self getArgumentStringFrom:aString toEnd:endFlag];
	} else {
		*prompt = YES;
		return [self getPromptFrom:aString toEnd:endFlag];
	}
	return nil;
}

- (MISC_Merge_Cond_Op)getConditionalFrom:(MiscString *)aString
/*" Attempts to parse a conditional from %{aString}.  Currently recognized
conditionals are:  <>, ><, !=, <=, =<, >=, =>, <, >, ==, =.  Returns the
type of conditional found or MISC_MCO_NONE if an unrecognized conditional
is found.  Removes the parsed conditional from %{aString}.
"*/
{
	MISC_Merge_Cond_Op operator = MISC_MCO_NONE;
	id chars = [MiscString newWithString:"=!<> \t\n"];
	id condString = [MiscString new];
	[aString trimLeadWhiteSpaces];
	while ([chars spotOf:[aString charAt:0]] >= 0) {
		[condString addChar:[aString charAt:0]];
		[aString removeFrom:0 length:1];
	}
	[condString replaceEveryOccurrenceOfChars:" \t\n" with:""];
	if ([condString length] > 2) {
		[self error_conditional:condString];
		return MISC_MCO_NONE;
	}
	if ([condString length] < 1) return MISC_MCO_NONE;
	if (![condString cmp:"<>" n:2]) {
		operator = MISC_MCO_NOTEQUAL;
	} else if (![condString cmp:"><" n:2]) {
		operator = MISC_MCO_NOTEQUAL;
	} else if (![condString cmp:"!=" n:2]) {
		operator = MISC_MCO_NOTEQUAL;
	} else if (![condString cmp:"<=" n:2]) {
		operator = MISC_MCO_LESSTHANOREQUAL;
	} else if (![condString cmp:">=" n:2]) {
		operator = MISC_MCO_GREATERTHANOREQUAL;
	} else if (![condString cmp:"=>" n:2]) {
		operator = MISC_MCO_GREATERTHANOREQUAL;
	} else if (![condString cmp:"=<" n:2]) {
		operator = MISC_MCO_LESSTHANOREQUAL;
	} else if (![condString cmp:">" n:1]) {
		operator = MISC_MCO_GREATERTHAN;
	} else if (![condString cmp:"<" n:1]) {
		operator = MISC_MCO_LESSTHAN;
	} else if (![condString cmp:"==" n:2]) {
		operator = MISC_MCO_EQUAL;
	} else if (![condString cmp:"=" n:1]) {
		operator = MISC_MCO_EQUAL;
	} else {
		[self error_conditional:condString];
		return MISC_MCO_NONE;
	}
	return operator;
}

- (void)error_conditional:(MiscString *)theCond
/*" This method is called if, while parsing, it is discovered that
a conditional is unrecognized.  Prints the name of the merge
command class, the text ªUnrecognized conditional:º, and the -#{stringValue}
of %{theCond} to the console.
"*/
{
	fprintf(stderr, "%s:  Unrecognized conditional:  \"%s\".\n",
			[[self class] name], [theCond stringValue]);
}

- (void)error_keyword:(MiscString *)aKeyWord
/*" This method is called if, while parsing, it is discovered that
a required key word is missing.  Prints the name of the merge
command class, the text ªMissing key word:º, and the -#{stringValue}
of %{aKeyWord} to the console.
"*/
{
	fprintf(stderr, "%s:  Missing key word:  \"%s\".\n",
			[[self class] name], [aKeyWord stringValue]);
}

- (void)error_noprompt
/*" This method is called if, while parsing, it is discovered that
the required prompt is missing.  (Referring to arguments that are
promptable.)  Prints the name of the merge
command class and the text ªMissing prompt.º to the console.
"*/
{
	fprintf(stderr, "%s:  Missing prompt.\n", [[self class] name]);
}

- (void)error_closequote
/*" This method is called if, while parsing, it is discovered that
quotations are not matched up properly.  Prints the name of the merge
command class and the text ªClosing quote missing or spurious extra
argument added.º to the console.
"*/
{
	fprintf(stderr,
		"%s:  Closing quote missing or spurious extra argument added.\n",
		[[self class] name]);
}

// Note:  if aCommand doesn't implement the MiscMergeCondCallback protocol,
// you'll get a NO back automatically.
+ (BOOL)evaluateConditionWith:(MiscMergeEngine *)anEngine
		for:aCommand
/*" Evaluates the condition in %{aCommand} in the context of the merge
in progress in %{anEngine}.  Returns YES if the condition evaluated true
and NO if not.  If %{aCommand} doesn't implement the MiscMergeCondCallback
protocol, you'll get a NO back automatically.  This method should be used
by MiscMergeCommand subclasses that need to evaluate conditionals as part
of their task, such as the "if" command.
"*/
{
	id v1, firstOperand, secondOperand;
	int comparison;

	// return NO if the callback isn't implemented...
	if (![aCommand conformsTo:@protocol(MiscMergeCondCallback)]) return NO;

	// we cache this return value to save us 4 calls...
	v1 = [aCommand value1];

	// special evaluators for first argument
	if (![v1 casecmp:"--NONE--"]) return NO;
	if (![v1 casecmp:"--NO--"]) return NO;
	if (![v1 casecmp:"--ALL--"]) return YES;
	if (![v1 casecmp:"--YES--"]) return YES;

	// turn the operands into their field values; literals will
	// come through unchanged...
	firstOperand = [anEngine getField:v1];
	secondOperand = [anEngine getField:[aCommand value2]];
	// string compare is default, so do it first
	comparison = [firstOperand compareTo:secondOperand];
	// if both start with a digit, then we'll override with a numerical comp.
	if ((NXIsDigit([firstOperand charAt:0]) ||
				([firstOperand charAt:0] == '-')) &&
			(NXIsDigit([secondOperand charAt:0]) ||
				([secondOperand charAt:0] == '-'))) {
		float firstOp = [firstOperand floatValue];
		float secondOp = [secondOperand floatValue];
		if (firstOp < secondOp) comparison = -1;
		else if (firstOp > secondOp) comparison = 1;
		else comparison = 0;
	}
	// now that we have comparison results, turn them into a YES/NO
	// depending upon the chosen operator.
	switch ([aCommand operator]) {
		case MISC_MCO_NONE : { // true if field is non-empty
			if ([firstOperand length] > 0) return YES;
			break;
		}
		case MISC_MCO_EQUAL : {
			if (!comparison) return YES;
			break;
		}
		case MISC_MCO_NOTEQUAL : {
			if (comparison) return YES;
			break;
		}
		case MISC_MCO_LESSTHANOREQUAL : {
			if (!comparison || (comparison < 0)) return YES;
			break;
		}
		case MISC_MCO_GREATERTHANOREQUAL : {
			if (!comparison || (comparison > 0)) return YES;
			break;
		}
		case MISC_MCO_LESSTHAN : {
			if (comparison < 0) return YES;
			break;
		}
		case MISC_MCO_GREATERTHAN : {
			if (comparison > 0) return YES;
			break;
		}
	}
	return NO;
}

@end
